/*
 * Copyright (C) 2006 Johan Maasing johan at zoom.nu Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package nu.zoom.catonine.stylerule;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import nu.zoom.catonine.xml.JAXBPatternAdapter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Collection of pattern and styles. This class is not thread safe.
 * 
 * @author "Johan Maasing" &lt;johan@zoom.nu&gt;
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = { "name", "guid", "logBlockPattern", "logBlockPatternBeginsBlock", "fileEncoding", "patternStyles" })
public class StyleRules implements Serializable, ChangeTracker {
	private static final long serialVersionUID = 1L;
	private transient Log log = LogFactory.getLog(getClass());
	private transient boolean changed = false;
	private transient ArrayList<StyleRulesListener> listeners = new ArrayList<StyleRulesListener>();
	private static final Pattern defaultPattern = Pattern.compile("((\\n\\r?)|(\\r\\n?))");

	private String name;
	private String guid;
	@XmlJavaTypeAdapter(JAXBPatternAdapter.class)
	private Pattern logBlockPattern;
	private boolean logBlockPatternBeginsBlock;
	private String fileEncoding;
	private ArrayList<StyleRule> patternStyles = new ArrayList<StyleRule>();

	StyleRules() {
		log.trace("Marshalling created a new style rule");
	}

	public StyleRules(String name) {
		this.name = name;
		guid = UUID.randomUUID().toString();
		logBlockPattern = defaultPattern;
		logBlockPatternBeginsBlock = false;
		fileEncoding = Charset.defaultCharset().name();
		log.trace("Created style rules named: " + name);
	}

	/**
	 * Get the current patterns.
	 * 
	 * @return A shallow copy of the list of patterns. Notice that the copy does
	 *         not change when the list changes structure.
	 */
	public synchronized List<StyleRule> getPatternStyles() {
		return new ArrayList<StyleRule>(patternStyles);
	}

	public synchronized void add(StyleRule style) {
		log.trace("Adding style rule " + style + " to style rules named: " + name);
		patternStyles.add(style);
		fireStructureChanged();
	}

	public synchronized void remove(StyleRule style) {
		log.trace("Removing style rule " + style + " from style rules named: " + name);
		patternStyles.remove(style);
		fireStructureChanged();
	}

	public synchronized void moveUp(StyleRule style) {
		int index = patternStyles.indexOf(style);
		if (index > 0) {
			log.trace("Moving style rule: " + style + " from index: " + index + " upwards ");
			patternStyles.remove(style);
			patternStyles.add(index - 1, style);
			fireStructureChanged();
		}
	}

	public synchronized void moveDown(StyleRule style) {
		int index = patternStyles.indexOf(style);
		if ((index > -1) && (index < patternStyles.size() - 1)) {
			log.trace("Moving style rule: " + style + " from index: " + index + " downwards.");
			patternStyles.remove(style);
			patternStyles.add(index + 1, style);
			fireStructureChanged();
		}
	}

	public String getGuid() {
		return guid;
	}

	public synchronized String getName() {
		return name;
	}

	public synchronized void setName(String newName) {
		log.trace("Changing style rules name from: " + name + " to " + newName);
		this.name = newName;
		fireListChangedName();
	}

	public synchronized void addListener(StyleRulesListener listener) {
		log.trace("Adding listener: " + listener);
		listeners.add(listener);
	}

	/**
	 * Add a listener to every style rule in the list.
	 * 
	 * @param listener
	 */
	public synchronized void addStyleRuleListener(StyleRuleListener listener) {
		log.trace("Adding style rule listener: " + listener + " to all style rules");
		for (StyleRule rule : patternStyles) {
			rule.addListener(listener);
		}
	}

	/**
	 * Remove a listener from every style rule in the list.
	 * 
	 * @param listener
	 */
	public synchronized void removeStyleRuleListener(StyleRuleListener listener) {
		log.trace("Removing style rule listener: " + listener + " from all style rules");
		for (StyleRule rule : patternStyles) {
			rule.removeListener(listener);
		}
	}

	public synchronized void removeListener(StyleRulesListener listener) {
		log.trace("Removing listener: " + listener);
		listeners.remove(listener);
	}

	private synchronized void fireStructureChanged() {
		log.trace("Informing listeners that structure changed");
		changed = true;
		ArrayList<Thread> notifierThreads = new ArrayList<Thread>();
		if (listeners.size() > 0) {
			for (final StyleRulesListener listener : listeners) {
				Thread t = new Thread(new Runnable() {

					@Override
					public void run() {
						listener.listStructureChanged(StyleRules.this);
					}
				});
				notifierThreads.add(t);
			}
		}
		for (Thread t : notifierThreads) {
			log.trace("Starting notifier thread");
			t.start();
		}
	}

	private synchronized void fireDataChanged() {
		log.trace("Informing listeners that structure changed");
		changed = true;
		ArrayList<Thread> notifierThreads = new ArrayList<Thread>();
		if (listeners.size() > 0) {
			for (final StyleRulesListener listener : listeners) {
				Thread t = new Thread(new Runnable() {

					@Override
					public void run() {
						listener.listDataChanged(StyleRules.this);
					}
				});
				notifierThreads.add(t);
			}
		}
		for (Thread t : notifierThreads) {
			log.trace("Starting notifier thread");
			t.start();
		}
	}

	private synchronized void fireListChangedName() {
		log.trace("Informing listeners that name changed");
		changed = true;
		ArrayList<Thread> notifierThreads = new ArrayList<Thread>();
		if (listeners.size() > 0) {
			for (final StyleRulesListener listener : listeners) {
				Thread t = new Thread(new Runnable() {

					@Override
					public void run() {
						listener.listChangedName(StyleRules.this);
					}
				});
				notifierThreads.add(t);
			}
		}
		for (Thread t : notifierThreads) {
			log.trace("Starting notifier thread");
			t.start();
		}
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof StyleRules) {
			return guid.equals(((StyleRules) obj).guid);
		} else {
			return false;
		}
	}

	@Override
	public int hashCode() {
		int hash = 3;
		hash = 47 * hash + (this.guid != null ? this.guid.hashCode() : 0);
		return hash;
	}

	@Override
	public String toString() {
		return name;
	}

	@Override
	public boolean isChanged() {
		if (!changed) {
			for (StyleRule rule : patternStyles) {
				if (rule.isChanged()) {
					log.trace("Style rules found a rule that is changed, setting changed status for collection");
					changed = true;
				}
			}
		}
		return changed;
	}

	@Override
	public void resetChangeStatus() {
		changed = false;
		log.trace("Resetting changed status for all rules");
		for (StyleRule rule : patternStyles) {
			rule.resetChangeStatus();
		}
	}

	/**
	 * @return Returns the logBlockPattern.
	 */
	public synchronized Pattern getLogBlockPattern() {
		return logBlockPattern;
	}

	/**
	 * @param logBlockPattern
	 *            The logBlockPattern to set.
	 */
	public synchronized void setLogBlockPattern(Pattern linePattern) {
		if ((linePattern != null) && (!linePattern.equals(this.logBlockPattern))) {
			this.logBlockPattern = linePattern;
			fireDataChanged();
		}
	}

	/**
	 * @return the logBlockPatternBeginsBlock
	 */
	public synchronized boolean isLogBlockPatternBeginsBlock() {
		return logBlockPatternBeginsBlock;
	}

	/**
	 * @param logBlockPatternBeginsBlock
	 *            the logBlockPatternBeginsBlock to set
	 */
	public synchronized void setLogBlockPatternBeginsBlock(boolean logBlockPatternBeginsBlock) {
		if (this.logBlockPatternBeginsBlock != logBlockPatternBeginsBlock) {
			this.logBlockPatternBeginsBlock = logBlockPatternBeginsBlock;
			fireDataChanged();
		}
	}

	/**
	 * @return the fileEncoding
	 */
	public synchronized String getFileEncoding() {
		return fileEncoding;
	}

	/**
	 * @param fileEncoding
	 *            the fileEncoding to set
	 */
	public synchronized void setFileEncoding(String fileEncoding) {
		if (!this.fileEncoding.equals(fileEncoding)) {
			this.fileEncoding = fileEncoding;
			fireDataChanged();
		}
	}
}
